#!/usr/bin/env python3
import sys, os, json, tempfile, shutil, subprocess, marshal, dis, zipfile, io, struct, ast, re, hashlib, zlib, importlib.util, types
from pathlib import Path
from datetime import datetime
from uuid import uuid4 as uniquename
from typing import List, Dict, Any, Optional, Tuple

try: import pefile
except ImportError: pefile = None
try: import lief
except ImportError: lief = None
try: import uncompyle6; UNCOMPYLE6_AVAILABLE = True
except ImportError: UNCOMPYLE6_AVAILABLE = False

from PyQt6.QtWidgets import *
from PyQt6.QtCore import *
from PyQt6.QtGui import *


class SearchBar(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent_window = parent
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0,0,0,0)
        layout.setSpacing(3)
        
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText("Search...")
        self.search_input.setFixedWidth(200)
        self.search_input.setStyleSheet("""
            QLineEdit { 
                background-color: #3C3C3C; 
                color: #CCC; 
                border: 1px solid #555; 
                padding: 3px 6px; 
                border-radius: 3px; 
                font-size: 11px; 
            } 
            QLineEdit:focus { 
                border: 1px solid #007ACC; 
            }
        """)
        self.search_input.returnPressed.connect(self.find_next)
        self.search_input.textChanged.connect(self.on_text_changed)
        
        self.case_btn = QPushButton("Aa")
        self.case_btn.setCheckable(True)
        self.case_btn.setFixedSize(26,22)
        self.regex_btn = QPushButton(".*")
        self.regex_btn.setCheckable(True)
        self.regex_btn.setFixedSize(26,22)
        self.prev_btn = QPushButton("◀")
        self.prev_btn.setFixedSize(22,22)
        self.prev_btn.clicked.connect(self.find_previous)
        self.next_btn = QPushButton("▶")
        self.next_btn.setFixedSize(22,22)
        self.next_btn.clicked.connect(self.find_next)
        self.count_label = QLabel("")
        self.count_label.setStyleSheet("color: #858585; font-size: 10px; min-width: 40px;")
        
        btn_style = """
            QPushButton { 
                background-color: #3A3A3A; 
                color: #CCC; 
                border: 1px solid #555; 
                border-radius: 2px; 
                font-size: 10px; 
            } 
            QPushButton:hover { 
                background-color: #4A4A4A; 
            } 
            QPushButton:checked { 
                background-color: #0E639C; 
                border: 1px solid #007ACC; 
            }
        """
        for btn in [self.case_btn, self.regex_btn, self.prev_btn, self.next_btn]: 
            btn.setStyleSheet(btn_style)
        for w in [self.search_input, self.case_btn, self.regex_btn, self.prev_btn, self.next_btn, self.count_label]: 
            layout.addWidget(w)
    
    def on_text_changed(self, text):
        self.find_next()
    
    def find_next(self):
        if self.parent_window:
            ed = self.parent_window.get_current_editor()
            if isinstance(ed, CodeEditor): 
                ed.search_text(self.search_input.text(), self.case_btn.isChecked(), False, self.regex_btn.isChecked(), True)
    
    def find_previous(self):
        if self.parent_window:
            ed = self.parent_window.get_current_editor()
            if isinstance(ed, CodeEditor): 
                ed.search_text(self.search_input.text(), self.case_btn.isChecked(), False, self.regex_btn.isChecked(), False)
    
    def update_count(self, c, t): 
        self.count_label.setText(f"{c}/{t}" if t > 0 else "")


class PYCAdvancedAnalyzer:
    PYTHON_MAGIC = {
        (20121,):(1,5),(50428,):(1,6),(50823,):(2,0),(60202,):(2,1),(60717,):(2,2),
        (62011,):(2,3),(62061,):(2,4),(62111,):(2,5),(62161,):(2,6),(62211,):(2,7),
        (3000,):(3,0),(3100,):(3,1),(3200,):(3,2),(3300,):(3,3),(3400,):(3,4),(3500,):(3,5),
        (3360,):(3,6),(3370,3371,3372,3373,3374,3375,3376,3377,3378,3379):(3,7),
        (3380,3381,3382,3383,3384,3385,3386,3387,3388,3389):(3,8),
        (3390,3391,3392,3393,3394,3395,3396,3397,3398,3399):(3,9),
        (3400,3401,3402,3403,3404,3405,3406,3407,3408,3409):(3,10),
        (3410,3411,3412,3413,3414,3415,3416,3417,3418,3419):(3,11)
    }
    
    @staticmethod
    def detect_version(pyc_path):
        try:
            with open(pyc_path,'rb') as f: 
                magic = f.read(4)
            mi = int.from_bytes(magic[:2],'little')
            for magics, ver in PYCAdvancedAnalyzer.PYTHON_MAGIC.items():
                if mi in magics: 
                    return ver
        except: 
            pass
        return (3,8)
    
    @staticmethod
    def header_size(ver):
        if ver[0] >= 3:
            if ver >= (3,7): return 16
            elif ver >= (3,3): return 12
            return 8
        return 8
    
    @staticmethod
    def extract_code(pyc_path):
        try:
            with open(pyc_path,'rb') as f:
                magic = f.read(4)
                if len(magic) < 4: return None, "Invalid magic"
                ver = PYCAdvancedAnalyzer.detect_version(pyc_path)
                hs = PYCAdvancedAnalyzer.header_size(ver)
                if ver[0] >= 3: f.read(hs-4)
                else: f.read(4)
                try: 
                    return marshal.load(f), f"Python {ver[0]}.{ver[1]}"
                except Exception as e: 
                    return None, str(e)
        except Exception as e: 
            return None, str(e)
    
    @staticmethod
    def get_bytecodes(pyc_path):
        output = io.StringIO()
        code, msg = PYCAdvancedAnalyzer.extract_code(pyc_path)
        if code is None: 
            return f"# Error: {msg}\n"
        output.write(f"# {msg} | Code: {code.co_name} | File: {code.co_filename}\n\n")
        try: 
            dis.dis(code, file=output)
        except: 
            pass
        for c in [c for c in code.co_consts if isinstance(c, types.CodeType)]:
            output.write(f"\n# Nested: {c.co_name}\n")
            try: 
                dis.dis(c, file=output)
            except: 
                pass
        return output.getvalue()


class BytecodeTranslator:
    
    @staticmethod
    def _safe_val(val):
        if isinstance(val, types.CodeType): return f"<code:{val.co_name}>"
        if isinstance(val, str): return repr(val)
        if isinstance(val, (int, float, bool, type(None))): return repr(val)
        if isinstance(val, bytes):
            try: return repr(val.decode('utf-8'))
            except: return repr(val)
        if isinstance(val, tuple):
            items = []
            for v in val:
                if isinstance(v, types.CodeType): items.append(f"<code:{v.co_name}>")
                elif isinstance(v, str): items.append(repr(v))
                else: items.append(repr(v))
            return f"({', '.join(items)})"
        return repr(val)
    
    @staticmethod
    def translate(text):
        lines = [l.strip() for l in text.split('\n') if l.strip() and not l.strip().startswith('#')]
        if not lines: return "# No bytecode found\n"
        
        result = []
        stack = []
        indent = 0
        
        for line in lines:
            m = re.match(r'\s*(\d+)\s+([A-Z_]+)\s*(.*)', line)
            if not m: continue
            
            instr = m.group(2)
            args = m.group(3).strip()
            arg_val = ""
            arg_match = re.search(r'\((.+)\)', args)
            if arg_match: arg_val = arg_match.group(1)
            prefix = "    " * indent
            
            if instr == 'LOAD_CONST':
                if arg_val and arg_val not in ('None','True','False','0',''):
                    try: stack.append(BytecodeTranslator._safe_val(ast.literal_eval(arg_val)))
                    except: stack.append(arg_val)
                elif arg_val == 'None': stack.append('None')
                elif arg_val == 'True': stack.append('True')
                elif arg_val == 'False': stack.append('False')
            elif instr == 'LOAD_FAST': stack.append(arg_val)
            elif instr == 'LOAD_GLOBAL': stack.append(arg_val)
            elif instr == 'LOAD_ATTR':
                if stack: stack[-1] += f".{arg_val}"
            elif instr == 'STORE_FAST':
                if stack: result.append(f"{prefix}{arg_val} = {stack.pop()}")
            elif instr == 'STORE_NAME':
                if stack: result.append(f"{prefix}{arg_val} = {stack.pop()}")
            elif instr == 'IMPORT_NAME':
                if arg_val:
                    result.append(f"{prefix}import {arg_val}")
                    if stack: stack.pop()
            elif instr == 'IMPORT_FROM':
                if stack: result.append(f"{prefix}from {stack.pop()} import {arg_val}")
            elif instr == 'CALL_FUNCTION':
                nargs = int(arg_val) if arg_val.isdigit() else 0
                args_list = [stack.pop() for _ in range(min(nargs, len(stack)))][::-1]
                func = stack.pop() if stack else '???'
                result.append(f"{prefix}{func}({', '.join(args_list)})")
            elif instr == 'RETURN_VALUE':
                val = stack.pop() if stack else 'None'
                result.append(f"{prefix}return {val}")
                if indent > 0: indent -= 1
            elif instr == 'POP_JUMP_IF_FALSE':
                if stack:
                    result.append(f"{prefix}if {stack.pop()}:")
                    indent += 1
            elif instr == 'COMPARE_OP':
                if len(stack) >= 2:
                    right = stack.pop()
                    left = stack.pop()
                    stack.append(f"{left} {arg_val} {right}")
            elif instr == 'BINARY_ADD':
                if len(stack) >= 2:
                    r = stack.pop()
                    l = stack.pop()
                    stack.append(f"({l} + {r})")
            elif instr == 'POP_TOP':
                if stack:
                    val = stack.pop()
                    if val and not val.startswith('<code:'):
                        result.append(f"{prefix}{val}")
            elif instr == 'FOR_ITER':
                if stack:
                    result.append(f"{prefix}for {arg_val} in {stack[-1]}:")
                    indent += 1
        
        if not result:
            return "# Translation: see bytecode above for details\n"
        return '\n'.join(result)


class PYCtoPYConverter:
    @staticmethod
    def convert(pyc_path, output_path=None):
        source = None
        method = ""
        if UNCOMPYLE6_AVAILABLE:
            try:
                out = io.StringIO()
                uncompyle6.decompile_file(pyc_path, out)
                source = out.getvalue()
                if source and len(source.strip()) > 10: method = "uncompyle6"
            except: pass
        if source is None:
            source = PYCAdvancedAnalyzer.get_bytecodes(pyc_path)
            method = "disassembly"
        translated = BytecodeTranslator.translate(source) if method == "disassembly" else source
        if source:
            if output_path:
                try:
                    with open(output_path, 'w', encoding='utf-8') as f: f.write(translated)
                except: pass
            return True, f"Decompiled: {method}", source, translated
        return False, "Failed", "", ""


class CTOCEntry:
    def __init__(self, p, cs, us, cf, tc, n):
        self.position=p
        self.cmprsdDataSize=cs
        self.uncmprsdDataSize=us
        self.cmprsFlag=cf
        self.typeCmprsData=tc
        self.name=n


class PyInstArchive:
    PYINST20_COOKIE_SIZE=24
    PYINST21_COOKIE_SIZE=24+64
    MAGIC=b'MEI\014\013\012\013\016'
    
    def __init__(self, path):
        self.filePath=path
        self.pycMagic=b'\0'*4
        self.barePycList=[]
    
    def open(self):
        try: self.fPtr=open(self.filePath,'rb'); self.fileSize=os.stat(self.filePath).st_size; return True
        except: return False
    
    def close(self):
        try: self.fPtr.close()
        except: pass
    
    def checkFile(self):
        ep=self.fileSize
        self.cookiePos=-1
        if ep<len(self.MAGIC): return False
        while True:
            sp=ep-8192 if ep>=8192 else 0
            cs=ep-sp
            if cs<len(self.MAGIC): break
            self.fPtr.seek(sp,0)
            data=self.fPtr.read(cs)
            offs=data.rfind(self.MAGIC)
            if offs!=-1: self.cookiePos=sp+offs; break
            ep=sp+len(self.MAGIC)-1
            if sp==0: break
        if self.cookiePos==-1: return False
        self.fPtr.seek(self.cookiePos+self.PYINST20_COOKIE_SIZE,0)
        self.pyinstVer=21 if b'python' in self.fPtr.read(64).lower() else 20
        return True
    
    def getCArchiveInfo(self):
        try:
            if self.pyinstVer==20:
                self.fPtr.seek(self.cookiePos,0)
                (magic,lop,toc,tocLen,pyver)=struct.unpack('!8siiii',self.fPtr.read(self.PYINST20_COOKIE_SIZE))
            else:
                self.fPtr.seek(self.cookiePos,0)
                (magic,lop,toc,tocLen,pyver,_)=struct.unpack('!8sIIii64s',self.fPtr.read(self.PYINST21_COOKIE_SIZE))
        except: return False
        self.pymaj,self.pymin=(pyver//100,pyver%100) if pyver>=100 else (pyver//10,pyver%10)
        tb=self.fileSize-self.cookiePos-(self.PYINST20_COOKIE_SIZE if self.pyinstVer==20 else self.PYINST21_COOKIE_SIZE)
        self.overlaySize=lop+tb
        self.overlayPos=self.fileSize-self.overlaySize
        self.tableOfContentsPos=self.overlayPos+toc
        self.tableOfContentsSize=tocLen
        return True
    
    def parseTOC(self):
        self.fPtr.seek(self.tableOfContentsPos,0)
        self.tocList=[]
        pl=0
        while pl<self.tableOfContentsSize:
            (es,)=struct.unpack('!i',self.fPtr.read(4))
            nl=struct.calcsize('!iIIIBc')
            (ep,cs,us,cf,tc,name)=struct.unpack(f'!IIIBc{es-nl}s',self.fPtr.read(es-4))
            try: name=name.decode("utf-8").rstrip("\0")
            except: name=str(uniquename())
            if name.startswith("/"): name=name.lstrip("/")
            if len(name)==0: name=str(uniquename())
            self.tocList.append(CTOCEntry(self.overlayPos+ep,cs,us,cf,tc,name))
            pl+=es
    
    def extractFiles(self,od,cb=None):
        if not os.path.exists(od): os.makedirs(od)
        total=len(self.tocList)
        for i,e in enumerate(self.tocList):
            if cb: cb(int(30+(i/total)*60),f"Extracting: {e.name}")
            self.fPtr.seek(e.position,0)
            data=self.fPtr.read(e.cmprsdDataSize)
            if e.cmprsFlag==1:
                try: data=zlib.decompress(data)
                except: continue
            if e.typeCmprsData in (b'd',b'o'): continue
            bp=os.path.dirname(e.name)
            if bp:
                fp=os.path.join(od,bp)
                if not os.path.exists(fp): os.makedirs(fp)
            if e.typeCmprsData==b's':
                if self.pycMagic==b'\0'*4: self.barePycList.append(e.name+'.pyc')
                self._writePyc(os.path.join(od,e.name+'.pyc'),data)
            elif e.typeCmprsData in (b'M',b'm'):
                if data[2:4]==b'\r\n':
                    if self.pycMagic==b'\0'*4: self.pycMagic=data[0:4]
                    self._writeRawData(os.path.join(od,e.name+'.pyc'),data)
                else:
                    if self.pycMagic==b'\0'*4: self.barePycList.append(e.name+'.pyc')
                    self._writePyc(os.path.join(od,e.name+'.pyc'),data)
            else:
                fp=os.path.join(od,e.name)
                self._writeRawData(fp,data)
                if e.typeCmprsData in (b'z',b'Z'): self._extractPyz(fp,od)
        self._fixBarePycs(od)
    
    def _writeRawData(self,fp,data):
        nm=fp.replace('\\',os.sep).replace('/',os.sep).replace('..','__')
        nd=os.path.dirname(nm)
        if nd and not os.path.exists(nd): os.makedirs(nd)
        with open(nm,'wb') as f: f.write(data)
    
    def _fixBarePycs(self,d):
        for pf in self.barePycList:
            fp=os.path.join(d,pf)
            if os.path.exists(fp):
                with open(fp,'r+b') as f: f.write(self.pycMagic)
    
    def _writePyc(self,fn,data):
        nm=fn.replace('\\',os.sep).replace('/',os.sep).replace('..','__')
        nd=os.path.dirname(nm)
        if nd and not os.path.exists(nd): os.makedirs(nd)
        with open(nm,'wb') as f:
            f.write(self.pycMagic)
            if self.pymaj>=3 and self.pymin>=7: f.write(b'\0'*4); f.write(b'\0'*8)
            else:
                f.write(b'\0'*4)
                if self.pymaj>=3 and self.pymin>=3: f.write(b'\0'*4)
            f.write(data)
    
    def _extractPyz(self,name,d):
        dn=os.path.join(d,name+'_extracted')
        if not os.path.exists(dn): os.mkdir(dn)
        with open(name,'rb') as f:
            assert f.read(4)==b'PYZ\0'
            pp=f.read(4)
            if self.pycMagic==b'\0'*4: self.pycMagic=pp
            (tp,)=struct.unpack('!i',f.read(4))
            f.seek(tp,0)
            try: toc=marshal.load(f)
            except: return
            if type(toc)==list: toc=dict(toc)
            for k in toc:
                (ispkg,pos,length)=toc[k]
                f.seek(pos,0)
                fn=k
                try: fn=fn.decode('utf-8')
                except: pass
                fn=fn.replace('..','__').replace('.',os.sep)
                fp=os.path.join(dn,fn,'__init__.pyc') if ispkg==1 else os.path.join(dn,fn+'.pyc')
                fd=os.path.dirname(fp)
                if not os.path.exists(fd): os.makedirs(fd)
                if length==0: self._writePyc(fp,b""); continue
                try: data=f.read(length); data=zlib.decompress(data); self._writePyc(fp,data)
                except: open(fp+'.encrypted','wb').write(data)


class PEAnalyzer:
    @staticmethod
    def analyze(filepath):
        if not pefile:
            return "<pre style='color:#F44747;'>pefile not installed. Run: pip install pefile</pre>"
        
        try:
            pe = pefile.PE(filepath)
            info = []
            
            info.append("<h2 style='color:#569CD6;margin:0;'>PE Analysis</h2>")
            
            info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>DOS Header</h3>")
            info.append(f"e_magic:                   {hex(pe.DOS_HEADER.e_magic)}")
            info.append(f"e_lfanew (PE offset):      {hex(pe.DOS_HEADER.e_lfanew)}")
            info.append(f"e_cblp:                    {pe.DOS_HEADER.e_cblp}")
            info.append(f"e_cp:                      {pe.DOS_HEADER.e_cp}")
            info.append(f"e_crlc:                    {pe.DOS_HEADER.e_crlc}")
            info.append(f"e_cparhdr:                 {pe.DOS_HEADER.e_cparhdr}")
            info.append(f"e_minalloc:                {pe.DOS_HEADER.e_minalloc}")
            info.append(f"e_maxalloc:                {pe.DOS_HEADER.e_maxalloc}")
            info.append(f"e_ss:                      {hex(pe.DOS_HEADER.e_ss)}")
            info.append(f"e_sp:                      {hex(pe.DOS_HEADER.e_sp)}")
            info.append(f"e_csum:                    {hex(pe.DOS_HEADER.e_csum)}")
            info.append(f"e_ip:                      {hex(pe.DOS_HEADER.e_ip)}")
            info.append(f"e_cs:                      {hex(pe.DOS_HEADER.e_cs)}")
            info.append(f"e_lfarlc:                  {hex(pe.DOS_HEADER.e_lfarlc)}")
            info.append(f"e_ovno:                    {pe.DOS_HEADER.e_ovno}")
            
            info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>File Header</h3>")
            machine_types = {
                0x014c: "IMAGE_FILE_MACHINE_I386 (i386)",
                0x0200: "IMAGE_FILE_MACHINE_IA64 (Intel Itanium)",
                0x8664: "IMAGE_FILE_MACHINE_AMD64 (x64)",
                0x01c4: "IMAGE_FILE_MACHINE_ARM",
                0xaa64: "IMAGE_FILE_MACHINE_ARM64",
                0x01c0: "IMAGE_FILE_MACHINE_ARMNT",
                0xebc: "IMAGE_FILE_MACHINE_EFI",
                0x0162: "IMAGE_FILE_MACHINE_R3000",
                0x0166: "IMAGE_FILE_MACHINE_R4000",
                0x0168: "IMAGE_FILE_MACHINE_R10000",
                0x0169: "IMAGE_FILE_MACHINE_WCEMIPSV2",
                0x0184: "IMAGE_FILE_MACHINE_ALPHA",
                0x01a2: "IMAGE_FILE_MACHINE_SH3",
                0x01a3: "IMAGE_FILE_MACHINE_SH3DSP",
                0x01a6: "IMAGE_FILE_MACHINE_SH4",
                0x01a8: "IMAGE_FILE_MACHINE_SH5",
                0x01c2: "IMAGE_FILE_MACHINE_THUMB",
                0x01d3: "IMAGE_FILE_MACHINE_AM33",
                0x01f0: "IMAGE_FILE_MACHINE_POWERPC",
                0x01f1: "IMAGE_FILE_MACHINE_POWERPCFP",
                0x0284: "IMAGE_FILE_MACHINE_ALPHA64",
                0x0366: "IMAGE_FILE_MACHINE_MIPSFPU",
                0x0466: "IMAGE_FILE_MACHINE_MIPSFPU16",
                0x0520: "IMAGE_FILE_MACHINE_TRICORE",
                0x0cef: "IMAGE_FILE_MACHINE_CEF",
                0x0ebc: "IMAGE_FILE_MACHINE_EBC",
                0x9041: "IMAGE_FILE_MACHINE_M32R",
                0xc0ee: "IMAGE_FILE_MACHINE_CEE"
            }
            machine = machine_types.get(pe.FILE_HEADER.Machine, f"Unknown (0x{pe.FILE_HEADER.Machine:04x})")
            info.append(f"Machine:                   {machine}")
            info.append(f"Number of Sections:        {pe.FILE_HEADER.NumberOfSections}")
            info.append(f"Timestamp:                 {datetime.fromtimestamp(pe.FILE_HEADER.TimeDateStamp)}")
            info.append(f"Pointer to Symbol Table:   {hex(pe.FILE_HEADER.PointerToSymbolTable)}")
            info.append(f"Number of Symbols:         {pe.FILE_HEADER.NumberOfSymbols}")
            info.append(f"Size of Optional Header:   {pe.FILE_HEADER.SizeOfOptionalHeader}")
            
            char_flags = {
                0x0001: "RELOCS_STRIPPED", 0x0002: "EXECUTABLE_IMAGE",
                0x0004: "LINE_NUMS_STRIPPED", 0x0008: "LOCAL_SYMS_STRIPPED",
                0x0010: "AGGRESSIVE_WS_TRIM", 0x0020: "LARGE_ADDRESS_AWARE",
                0x0040: "RESERVED", 0x0080: "BYTES_REVERSED_LO",
                0x0100: "32BIT_MACHINE", 0x0200: "DEBUG_STRIPPED",
                0x0400: "REMOVABLE_RUN_FROM_SWAP", 0x0800: "NET_RUN_FROM_SWAP",
                0x1000: "SYSTEM", 0x2000: "DLL",
                0x4000: "UP_SYSTEM_ONLY", 0x8000: "BYTES_REVERSED_HI"
            }
            chars = [name for flag, name in char_flags.items() if pe.FILE_HEADER.Characteristics & flag]
            info.append(f"Characteristics:           {', '.join(chars) if chars else 'None'}")
            
            info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Optional Header</h3>")
            magic_str = "PE32 (32-bit)" if pe.OPTIONAL_HEADER.Magic == 0x10b else "PE32+ (64-bit)" if pe.OPTIONAL_HEADER.Magic == 0x20b else f"ROM ({hex(pe.OPTIONAL_HEADER.Magic)})"
            info.append(f"Magic:                     {magic_str}")
            info.append(f"Linker Version:            {pe.OPTIONAL_HEADER.MajorLinkerVersion}.{pe.OPTIONAL_HEADER.MinorLinkerVersion}")
            info.append(f"Size of Code:              {pe.OPTIONAL_HEADER.SizeOfCode} bytes")
            info.append(f"Size of Initialized Data:  {pe.OPTIONAL_HEADER.SizeOfInitializedData} bytes")
            info.append(f"Size of Uninitialized Data:{pe.OPTIONAL_HEADER.SizeOfUninitializedData} bytes")
            info.append(f"Entry Point RVA:           {hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint)}")
            info.append(f"Base of Code:              {hex(pe.OPTIONAL_HEADER.BaseOfCode)}")
            if hasattr(pe.OPTIONAL_HEADER, 'BaseOfData'):
                info.append(f"Base of Data:              {hex(pe.OPTIONAL_HEADER.BaseOfData)}")
            info.append(f"Image Base:                {hex(pe.OPTIONAL_HEADER.ImageBase)}")
            info.append(f"Section Alignment:         {hex(pe.OPTIONAL_HEADER.SectionAlignment)}")
            info.append(f"File Alignment:            {hex(pe.OPTIONAL_HEADER.FileAlignment)}")
            info.append(f"OS Version:                {pe.OPTIONAL_HEADER.MajorOperatingSystemVersion}.{pe.OPTIONAL_HEADER.MinorOperatingSystemVersion}")
            info.append(f"Image Version:             {pe.OPTIONAL_HEADER.MajorImageVersion}.{pe.OPTIONAL_HEADER.MinorImageVersion}")
            info.append(f"Subsystem Version:         {pe.OPTIONAL_HEADER.MajorSubsystemVersion}.{pe.OPTIONAL_HEADER.MinorSubsystemVersion}")
            info.append(f"Size of Image:             {pe.OPTIONAL_HEADER.SizeOfImage} bytes")
            info.append(f"Size of Headers:           {pe.OPTIONAL_HEADER.SizeOfHeaders} bytes")
            info.append(f"Checksum:                  {hex(pe.OPTIONAL_HEADER.CheckSum)}")
            
            subsystem_types = {
                0: "IMAGE_SUBSYSTEM_UNKNOWN",
                1: "IMAGE_SUBSYSTEM_NATIVE",
                2: "IMAGE_SUBSYSTEM_WINDOWS_GUI",
                3: "IMAGE_SUBSYSTEM_WINDOWS_CUI",
                5: "IMAGE_SUBSYSTEM_OS2_CUI",
                7: "IMAGE_SUBSYSTEM_POSIX_CUI",
                8: "IMAGE_SUBSYSTEM_NATIVE_WINDOWS",
                9: "IMAGE_SUBSYSTEM_WINDOWS_CE_GUI",
                10: "IMAGE_SUBSYSTEM_EFI_APPLICATION",
                11: "IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER",
                12: "IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER",
                13: "IMAGE_SUBSYSTEM_EFI_ROM",
                14: "IMAGE_SUBSYSTEM_XBOX",
                16: "IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION"
            }
            subsystem = subsystem_types.get(pe.OPTIONAL_HEADER.Subsystem, f"Unknown ({pe.OPTIONAL_HEADER.Subsystem})")
            info.append(f"Subsystem:                 {subsystem}")
            
            dll_char_flags = {
                0x0020: "HIGH_ENTROPY_VA", 0x0040: "DYNAMIC_BASE",
                0x0080: "FORCE_INTEGRITY", 0x0100: "NX_COMPAT",
                0x0200: "NO_ISOLATION", 0x0400: "NO_SEH",
                0x0800: "NO_BIND", 0x1000: "APPCONTAINER",
                0x2000: "WDM_DRIVER", 0x4000: "TERMINAL_SERVER_AWARE",
                0x8000: "GUARD_CF"
            }
            dll_chars = [name for flag, name in dll_char_flags.items() if pe.OPTIONAL_HEADER.DllCharacteristics & flag]
            info.append(f"DLL Characteristics:       {', '.join(dll_chars) if dll_chars else 'None'}")
            
            info.append(f"Stack Reserve:             {hex(pe.OPTIONAL_HEADER.SizeOfStackReserve)}")
            info.append(f"Stack Commit:              {hex(pe.OPTIONAL_HEADER.SizeOfStackCommit)}")
            info.append(f"Heap Reserve:              {hex(pe.OPTIONAL_HEADER.SizeOfHeapReserve)}")
            info.append(f"Heap Commit:               {hex(pe.OPTIONAL_HEADER.SizeOfHeapCommit)}")
            info.append(f"Loader Flags:              {hex(pe.OPTIONAL_HEADER.LoaderFlags)}")
            info.append(f"Number of Data Directories:{pe.OPTIONAL_HEADER.NumberOfRvaAndSizes}")
            
            data_dir_names = [
                "EXPORT", "IMPORT", "RESOURCE", "EXCEPTION", "SECURITY",
                "BASERELOC", "DEBUG", "ARCHITECTURE", "GLOBALPTR", "TLS",
                "LOAD_CONFIG", "BOUND_IMPORT", "IAT", "DELAY_IMPORT", "COM_DESCRIPTOR"
            ]
            info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Data Directories</h3>")
            for i, dir_entry in enumerate(pe.OPTIONAL_HEADER.DATA_DIRECTORY):
                if dir_entry.VirtualAddress > 0 or dir_entry.Size > 0:
                    name = data_dir_names[i] if i < len(data_dir_names) else f"UNKNOWN_{i}"
                    info.append(f"{name:20} RVA={hex(dir_entry.VirtualAddress):10} Size={dir_entry.Size}")
            
            info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Sections</h3>")
            for section in pe.sections:
                name = section.Name.decode('utf-8').rstrip('\x00')
                sec_char_flags = {
                    0x00000008: "NO_PAD", 0x00000020: "CODE",
                    0x00000040: "INITIALIZED_DATA", 0x00000080: "UNINITIALIZED_DATA",
                    0x00000100: "LNK_OTHER", 0x00000200: "LNK_INFO",
                    0x00000800: "LNK_REMOVE", 0x00001000: "LNK_COMDAT",
                    0x00004000: "NO_DEFER_SPEC_EXC", 0x00008000: "GPREL",
                    0x02000000: "DISCARDABLE", 0x04000000: "NOT_CACHED",
                    0x08000000: "NOT_PAGED", 0x10000000: "SHARED",
                    0x20000000: "EXECUTE", 0x40000000: "READ",
                    0x80000000: "WRITE"
                }
                sec_chars = [name for flag, name in sec_char_flags.items() if section.Characteristics & flag]
                
                info.append(f"<b style='color:#CE9178;'>{name}</b>:")
                info.append(f"  Virtual Address:          {hex(section.VirtualAddress)}")
                info.append(f"  Virtual Size:             {hex(section.Misc_VirtualSize)}")
                info.append(f"  Raw Size:                 {hex(section.SizeOfRawData)}")
                info.append(f"  Raw Offset:               {hex(section.PointerToRawData)}")
                info.append(f"  Characteristics:          {', '.join(sec_chars)}")
                info.append("")
            
            if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
                info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Imports</h3>")
                for entry in pe.DIRECTORY_ENTRY_IMPORT:
                    dll_name = entry.dll.decode('utf-8') if entry.dll else 'Unknown'
                    info.append(f"<b style='color:#CE9178;'>{dll_name}</b>:")
                    for imp in entry.imports[:20]:
                        if imp.name:
                            info.append(f"  {imp.name.decode('utf-8')}")
                        else:
                            info.append(f"  Ordinal: {imp.ordinal}")
                    if len(entry.imports) > 20:
                        info.append(f"  <span style='color:#858585;'>... and {len(entry.imports) - 20} more</span>")
                    info.append("")
            
            if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
                info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Exports</h3>")
                exp = pe.DIRECTORY_ENTRY_EXPORT
                if exp.name:
                    info.append(f"Name:                      {exp.name.decode('utf-8')}")
                info.append(f"Number of Functions:       {len(exp.symbols)}")
            
            pe.close()
            return f"<pre style='color:#D4D4D4;font-family:Consolas;font-size:11px;'>" + "\n".join(info) + "</pre>"
        except Exception as e:
            return f"<pre style='color:#F44747;'>Error analyzing PE: {str(e)}</pre>"


class ELFAnalyzer:
    @staticmethod
    def analyze(filepath):
        if not lief:
            return "<pre style='color:#F44747;'>LIEF not installed. Run: pip install lief</pre>"
        
        try:
            with open(filepath, 'rb') as f:
                header = f.read(4)
            if header[:4] != b'\x7fELF':
                return "<pre style='color:#F44747;'>Not a valid ELF file (missing ELF magic)</pre>"
            
            binary = lief.parse(filepath)
            if not binary:
                return "<pre style='color:#F44747;'>Failed to parse binary file</pre>"
            
            if not isinstance(binary, lief.ELF.Binary):
                return "<pre style='color:#F44747;'>Not a valid ELF file (detected as other format)</pre>"
            
            info = []
            info.append("<h2 style='color:#569CD6;margin:0;'>ELF Analysis</h2>")
            
            info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>ELF Header</h3>")
            
            elf_class = "ELF64" if binary.header.identity_class == lief.ELF.ELF_CLASS.ELFCLASS64 else "ELF32"
            info.append(f"Class:                     {elf_class}")
            
            endian = "Little Endian" if binary.header.identity_data == lief.ELF.ELF_DATA.ELFDATA2LSB else "Big Endian"
            info.append(f"Data:                      {endian}")
            
            osabi_map = {
                lief.ELF.ELF_OSABI.NONE: "UNIX System V",
                lief.ELF.ELF_OSABI.LINUX: "Linux",
                lief.ELF.ELF_OSABI.FREEBSD: "FreeBSD",
                lief.ELF.ELF_OSABI.ARM_AEABI: "ARM EABI"
            }
            osabi = osabi_map.get(binary.header.identity_osabi, f"Other (0x{binary.header.identity_osabi:x})")
            info.append(f"OS/ABI:                    {osabi}")
            
            type_map = {
                lief.ELF.E_TYPE.ET_REL: "REL (Relocatable)",
                lief.ELF.E_TYPE.ET_EXEC: "EXEC (Executable)",
                lief.ELF.E_TYPE.ET_DYN: "DYN (Shared Object)",
                lief.ELF.E_TYPE.ET_CORE: "CORE"
            }
            elf_type = type_map.get(binary.header.file_type, f"Unknown ({binary.header.file_type})")
            info.append(f"Type:                      {elf_type}")
            
            machine_map = {
                lief.ELF.ARCH.i386: "Intel 80386",
                lief.ELF.ARCH.x86_64: "AMD x86-64",
                lief.ELF.ARCH.ARM: "ARM",
                lief.ELF.ARCH.AARCH64: "ARM AARCH64",
                lief.ELF.ARCH.MIPS: "MIPS",
                lief.ELF.ARCH.PPC: "PowerPC",
                lief.ELF.ARCH.PPC64: "PowerPC64"
            }
            machine = machine_map.get(binary.header.machine_type, f"Unknown ({binary.header.machine_type})")
            info.append(f"Machine:                   {machine}")
            info.append(f"Entry Point:               {hex(binary.header.entrypoint)}")
            info.append(f"Program Header Offset:     {binary.header.program_header_offset}")
            info.append(f"Section Header Offset:     {binary.header.section_header_offset}")
            info.append(f"ELF Header Size:           {binary.header.header_size} bytes")
            info.append(f"Number of Program Headers: {binary.header.numberof_segments}")
            info.append(f"Number of Section Headers: {binary.header.numberof_sections}")
            
            if binary.segments:
                info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Segments</h3>")
                for i, seg in enumerate(binary.segments):
                    type_map = {
                        lief.ELF.SEGMENT_TYPES.LOAD: "LOAD",
                        lief.ELF.SEGMENT_TYPES.DYNAMIC: "DYNAMIC",
                        lief.ELF.SEGMENT_TYPES.INTERP: "INTERP",
                        lief.ELF.SEGMENT_TYPES.NOTE: "NOTE",
                        lief.ELF.SEGMENT_TYPES.GNU_EH_FRAME: "GNU_EH_FRAME",
                        lief.ELF.SEGMENT_TYPES.GNU_STACK: "GNU_STACK",
                        lief.ELF.SEGMENT_TYPES.GNU_RELRO: "GNU_RELRO"
                    }
                    seg_type = type_map.get(seg.type, f"Unknown (0x{seg.type:x})")
                    
                    seg_flags = ""
                    if seg.has(lief.ELF.SEGMENT_FLAGS.R): seg_flags += "R"
                    if seg.has(lief.ELF.SEGMENT_FLAGS.W): seg_flags += "W"
                    if seg.has(lief.ELF.SEGMENT_FLAGS.X): seg_flags += "X"
                    
                    info.append(f"<b style='color:#CE9178;'>[{i}] {seg_type}</b> Flags={seg_flags}:")
                    info.append(f"  Virtual Address:          {hex(seg.virtual_address)}")
                    info.append(f"  Virtual Size:             {hex(seg.virtual_size)}")
                    info.append(f"  File Offset:              {hex(seg.file_offset)}")
                    info.append(f"  File Size:                {hex(seg.file_size)}")
                    info.append("")
            
            if binary.sections:
                info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Sections</h3>")
                for section in binary.sections:
                    if section.name:
                        info.append(f"<b style='color:#CE9178;'>{section.name}</b>:")
                        info.append(f"  Virtual Address:          {hex(section.virtual_address)}")
                        info.append(f"  Size:                     {hex(section.size)}")
                        info.append(f"  Offset:                   {hex(section.offset)}")
                        info.append("")
            
            if binary.has(lief.ELF.DYNAMIC_ENTRIES):
                info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Dynamic Entries</h3>")
                for entry in binary.dynamic_entries:
                    tag = str(entry.tag).replace('Tag.', '')
                    if hasattr(entry, 'value'):
                        info.append(f"{tag:30} {hex(entry.value)}")
            
            if binary.has(lief.ELF.DYNAMIC_SYMBOLS):
                info.append("<h3 style='color:#4EC9B0;margin:10px 0 5px 0;'>Dynamic Symbols</h3>")
                for sym in binary.dynamic_symbols[:30]:
                    if sym.name:
                        info.append(f"{sym.name:40} {hex(sym.value)}")
            
            return f"<pre style='color:#D4D4D4;font-family:Consolas;font-size:11px;'>" + "\n".join(info) + "</pre>"
        except Exception as e:
            return f"<pre style='color:#F44747;'>Error analyzing ELF: {str(e)}</pre>"


class BinaryAnalyzer:
    @staticmethod
    def detect_and_analyze(filepath):
        results = []
        
        try:
            with open(filepath, 'rb') as f:
                header = f.read(64)
            
            if header[:2] == b'MZ':
                results.append(("PE Analysis", PEAnalyzer.analyze(filepath)))
            
            if header[:4] == b'\x7fELF':
                results.append(("ELF Analysis", ELFAnalyzer.analyze(filepath)))
            
            if not results:
                results.append(("Binary Info", f"<pre style='color:#D4D4D4;font-family:Consolas;font-size:11px;'>File: {os.path.basename(filepath)}\nSize: {os.path.getsize(filepath)} bytes\nMD5: {hashlib.md5(open(filepath,'rb').read()).hexdigest()}\nSHA256: {hashlib.sha256(open(filepath,'rb').read()).hexdigest()}</pre>"))
        
        except Exception as e:
            results.append(("Error", f"<pre style='color:#F44747;'>Error: {str(e)}</pre>"))
        
        return results


class CodeHighlighter(QSyntaxHighlighter):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.rules=[]
        kw=QTextCharFormat()
        kw.setForeground(QColor("#569CD6"))
        kw.setFontWeight(QFont.Weight.Bold)
        for w in ["and","as","assert","break","class","continue","def","del","elif","else","except",
                   "finally","for","from","global","if","import","in","is","lambda","nonlocal","not",
                   "or","pass","raise","return","try","while","with","yield","True","False","None","self","super"]:
            self.rules.append((f"\\b{w}\\b",kw))
        s=QTextCharFormat()
        s.setForeground(QColor("#CE9178"))
        self.rules.append(('"[^"]*"',s))
        self.rules.append(("'[^']*'",s))
        c=QTextCharFormat()
        c.setForeground(QColor("#6A9955"))
        self.rules.append(("#[^\n]*",c))
        n=QTextCharFormat()
        n.setForeground(QColor("#B5CEA8"))
        self.rules.append(("\\b[0-9]+\\b",n))
    
    def highlightBlock(self, text):
        for p,f in self.rules:
            for m in re.finditer(p,text):
                self.setFormat(m.start(),m.end()-m.start(),f)


class LineNumberArea(QWidget):
    def __init__(self, editor):
        super().__init__(editor)
        self.editor=editor
    
    def sizeHint(self):
        return QSize(self.editor.lineNumberAreaWidth(),0)
    
    def paintEvent(self, e):
        self.editor.lineNumberAreaPaintEvent(e)


class CodeEditor(QPlainTextEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.lna=LineNumberArea(self)
        self.setFont(QFont("Consolas",11))
        self.setTabStopDistance(QFontMetrics(self.font()).horizontalAdvance(' ')*4)
        self.setStyleSheet("""
            QPlainTextEdit { 
                background-color: #1E1E1E; 
                color: #D4D4D4; 
                border: 1px solid #3F3F3F; 
                selection-background-color: #264F78; 
                font-size: 12px; 
            }
        """)
        self.blockCountChanged.connect(self.updateLNAW)
        self.updateRequest.connect(self.updateLNA)
        self.cursorPositionChanged.connect(self.highlightCL)
        self.updateLNAW(0)
        self.highlightCL()
        self.highlighter=CodeHighlighter(self.document())
        self.setAcceptDrops(False)
        self.setUndoRedoEnabled(True)
        self._sr=[]
        self._csi=-1
        self._sb=None
    
    def set_search_bar(self, sb):
        self._sb=sb
    
    def lineNumberAreaWidth(self): 
        return 3+self.fontMetrics().horizontalAdvance('9')*len(str(max(1,self.blockCount())))
    
    def updateLNAW(self,_):
        self.setViewportMargins(self.lineNumberAreaWidth(),0,0,0)
    
    def updateLNA(self,rect,dy):
        if dy: self.lna.scroll(0,dy)
        else: self.lna.update(0,rect.y(),self.lna.width(),rect.height())
        if rect.contains(self.viewport().rect()): self.updateLNAW(0)
    
    def resizeEvent(self,e):
        super().resizeEvent(e)
        cr=self.contentsRect()
        self.lna.setGeometry(QRect(cr.left(),cr.top(),self.lineNumberAreaWidth(),cr.height()))
    
    def highlightCL(self):
        es=[]
        if not self.isReadOnly():
            s=QTextEdit.ExtraSelection()
            s.format.setBackground(QColor("#2A2A2A"))
            s.format.setProperty(QTextCharFormat.Property.FullWidthSelection,True)
            s.cursor=self.textCursor()
            s.cursor.clearSelection()
            es.append(s)
        self.setExtraSelections(es)
    
    def lineNumberAreaPaintEvent(self,e):
        p=QPainter(self.lna)
        p.fillRect(e.rect(),QColor("#1E1E1E"))
        b=self.firstVisibleBlock()
        bn=b.blockNumber()
        t=self.blockBoundingGeometry(b).translated(self.contentOffset()).top()
        bt=t+self.blockBoundingRect(b).height()
        while b.isValid() and t<=e.rect().bottom():
            if b.isVisible() and bt>=e.rect().top():
                p.setPen(QColor("#858585"))
                p.drawText(0,int(t),self.lna.width()-5,self.fontMetrics().height(),
                          Qt.AlignmentFlag.AlignRight,str(bn+1))
            b=b.next()
            t=bt
            bt=t+self.blockBoundingRect(b).height()
            bn+=1
    
    def insertFromMimeData(self,s):
        if s.hasText(): 
            t=s.text()
            c=self.textCursor()
            c.insertText(t)
            self.setTextCursor(c)
        else: 
            super().insertFromMimeData(s)
    
    def search_text(self,text,cs,ww,rx,forward=True):
        self._sr=[]
        self._csi=-1
        if not text:
            if self._sb: self._sb.update_count(0,0)
            self.setExtraSelections([])
            return
        content=self.toPlainText()
        try:
            if rx:
                f=0 if cs else re.IGNORECASE
                matches=[(m.start(),m.end()) for m in re.finditer(re.compile(text,f),content)]
            else:
                st=text if cs else text.lower()
                cts=content if cs else content.lower()
                matches=[]
                start=0
                while True:
                    idx=cts.find(st,start)
                    if idx==-1: break
                    matches.append((idx,idx+len(text)))
                    start=idx+1
            self._sr=matches
            if matches:
                self._csi=0 if forward else len(matches)-1
                self._highlight()
                es=[]
                for s,e in matches:
                    c=self.textCursor()
                    c.setPosition(s)
                    c.setPosition(e,QTextCursor.MoveMode.KeepAnchor)
                    sel=QTextEdit.ExtraSelection()
                    sel.format.setBackground(QColor("#515C6A"))
                    sel.format.setForeground(QColor("#FFF"))
                    sel.cursor=c
                    es.append(sel)
                self.setExtraSelections(es)
            else:
                self.setExtraSelections([])
            if self._sb: self._sb.update_count(self._csi+1 if matches else 0,len(matches))
        except:
            pass
    
    def _highlight(self):
        if 0<=self._csi<len(self._sr):
            s,e=self._sr[self._csi]
            c=self.textCursor()
            c.setPosition(s)
            c.setPosition(e,QTextCursor.MoveMode.KeepAnchor)
            self.setTextCursor(c)
            self.ensureCursorVisible()


class EditorTabWidget(QTabWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setTabsClosable(True)
        self.setMovable(True)
        self.setDocumentMode(True)
        self.tabCloseRequested.connect(self.close_tab)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_tab_context)
        
        self.add_btn = QPushButton("+")
        self.add_btn.setFixedSize(24, 24)
        self.add_btn.setStyleSheet("""
            QPushButton {
                background-color: #3A3A3A;
                color: #CCC;
                border: 1px solid #555;
                border-radius: 2px;
                font-size: 14px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #4A4A4A;
            }
        """)
        self.add_btn.clicked.connect(self.add_new_tab)
        self.setCornerWidget(self.add_btn, Qt.Corner.TopRightCorner)
        
        self.parent_window = parent
    
    def add_new_tab(self):
        ed = CodeEditor()
        ed.set_search_bar(self.parent_window.search_bar)
        ed.setProperty('modified', False)
        ed.textChanged.connect(lambda: self.parent_window.on_mod(ed))
        ed.cursorPositionChanged.connect(lambda: self.parent_window.on_cursor(ed))
        
        tab_count = self.count() + 1
        tab_name = f"Untitled-{tab_count}"
        self.addTab(ed, tab_name)
        self.setCurrentWidget(ed)
    
    def close_tab(self, idx):
        w = self.widget(idx)
        if isinstance(w, CodeEditor) and w.property('modified'):
            fp = w.property('file_path')
            r = QMessageBox.question(self, "Unsaved", "Save changes?", 
                                   QMessageBox.StandardButton.Save | 
                                   QMessageBox.StandardButton.Discard | 
                                   QMessageBox.StandardButton.Cancel)
            if r == QMessageBox.StandardButton.Save:
                if fp:
                    self.parent_window._save(w, fp)
            elif r == QMessageBox.StandardButton.Cancel:
                return
        self.removeTab(idx)
    
    def show_tab_context(self, pos):
        tab_idx = self.tabBar().tabAt(pos)
        if tab_idx < 0:
            return
        
        menu = QMenu(self)
        menu.setStyleSheet("""
            QMenu { 
                background-color: #2D2D2D; 
                color: #CCC; 
                border: 1px solid #454545; 
            } 
            QMenu::item:selected { 
                background-color: #094771; 
            }
        """)
        
        menu.addAction("Close", lambda: self.close_tab(tab_idx))
        menu.addAction("Close Others", lambda: self.close_other_tabs(tab_idx))
        menu.addAction("Close All", lambda: self.close_all_tabs())
        menu.addSeparator()
        menu.addAction("Copy Path", lambda: self.copy_tab_path(tab_idx))
        
        menu.exec(self.tabBar().mapToGlobal(pos))
    
    def close_other_tabs(self, keep_idx):
        for i in range(self.count() - 1, -1, -1):
            if i != keep_idx:
                self.close_tab(i)
    
    def close_all_tabs(self):
        for i in range(self.count() - 1, -1, -1):
            self.close_tab(i)
    
    def copy_tab_path(self, idx):
        w = self.widget(idx)
        if isinstance(w, CodeEditor):
            fp = w.property('file_path')
            if fp:
                QApplication.clipboard().setText(fp)


class ExtractorThread(QThread):
    progress = pyqtSignal(int, str)
    finished = pyqtSignal(dict)
    error = pyqtSignal(str)
    
    def __init__(self, fp):
        super().__init__()
        self.filepath=fp
    
    def run(self):
        try:
            ed = os.path.join(tempfile.gettempdir(), f"yhujinpy_{os.path.basename(self.filepath)}_extracted")
            self.progress.emit(5, "Checking file...")
            arch = PyInstArchive(self.filepath)
            if not arch.open():
                self.progress.emit(40, "Generic extraction...")
                os.makedirs(ed, exist_ok=True)
                ext = os.path.splitext(self.filepath)[1].lower()
                if ext in ('.pyc', '.pyo'):
                    code, msg = PYCAdvancedAnalyzer.extract_code(self.filepath)
                    if code:
                        with open(os.path.join(ed, 'analysis.txt'), 'w') as f: 
                            f.write(PYCAdvancedAnalyzer.get_bytecodes(self.filepath))
                    shutil.copy2(self.filepath, os.path.join(ed, os.path.basename(self.filepath)))
                    self.progress.emit(100, "Done")
                    self.finished.emit({'type': 'Python Bytecode', 'extract_dir': ed, 'python_version': msg})
                    return
                elif ext == '.zip':
                    with zipfile.ZipFile(self.filepath, 'r') as zf: zf.extractall(ed)
                    self.progress.emit(100, "Done")
                    self.finished.emit({'type': 'ZIP', 'extract_dir': ed})
                    return
                else:
                    with open(self.filepath, 'rb') as f: data = f.read()
                    with open(os.path.join(ed, 'info.txt'), 'w') as f: 
                        f.write(f"File: {os.path.basename(self.filepath)}\nSize: {len(data)} bytes\nMD5: {hashlib.md5(data).hexdigest()}\nSHA256: {hashlib.sha256(data).hexdigest()}")
                    self.progress.emit(100, "Done")
                    self.finished.emit({'type': 'Binary', 'extract_dir': ed, 'size': len(data)})
                    return
            self.progress.emit(10, "Checking PyInstaller...")
            if not arch.checkFile(): arch.close(); self.error.emit("Not valid PyInstaller"); return
            self.progress.emit(20, "Reading archive...")
            if not arch.getCArchiveInfo(): arch.close(); self.error.emit("Failed reading archive"); return
            self.progress.emit(25, "Parsing TOC...")
            arch.parseTOC()
            self.progress.emit(30, f"Extracting {len(arch.tocList)} files...")
            arch.extractFiles(ed, lambda p, s: self.progress.emit(p, s))
            arch.close()
            self.progress.emit(95, "Finalizing...")
            self.progress.emit(100, "Done")
            self.finished.emit({'type': 'PyInstaller EXE', 'python_version': f"{arch.pymaj}.{arch.pymin}", 
                              'extract_dir': ed, 'files_count': len(arch.tocList)})
        except Exception as e:
            self.error.emit(str(e))


class YhujinPy(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("YhujinPy - PyInstaller Extractor")
        self.setMinimumSize(950, 600)
        self.resize(1100, 700)
        self.setAcceptDrops(True)
        self.setStyleSheet("""
            QMainWindow { background-color: #1E1E1E; }
            QMenuBar { background-color: #2D2D2D; color: #CCC; border-bottom: 1px solid #3F3F3F; padding: 2px; font-size: 12px; }
            QMenuBar::item:selected { background-color: #37373D; }
            QMenu { background-color: #2D2D2D; color: #CCC; border: 1px solid #454545; font-size: 12px; }
            QMenu::item:selected { background-color: #094771; }
            QToolBar { background-color: #2D2D2D; border-bottom: 1px solid #3F3F3F; spacing: 3px; padding: 2px; }
            QStatusBar { background-color: #007ACC; color: white; font-size: 11px; padding: 2px; }
            QSplitter::handle { background-color: #454545; width: 2px; }
            QTreeWidget { 
                background-color: #252526; 
                color: #CCC; 
                border: 1px solid #3F3F3F; 
                font-size: 12px; 
            }
            QTreeWidget::item { padding: 2px; }
            QTreeWidget::item:selected { background-color: #094771; color: white; }
            QTreeWidget::item:hover { background-color: #2A2D2E; }
            QHeaderView::section { 
                background-color: #2D2D2D; 
                color: #CCC; 
                border: 1px solid #3F3F3F; 
                padding: 3px; 
                font-size: 12px; 
            }
            QScrollBar:vertical { background-color: #1E1E1E; width: 10px; }
            QScrollBar::handle:vertical { background-color: #424242; min-height: 20px; }
            QScrollBar::handle:vertical:hover { background-color: #4F4F4F; }
            QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
        """)
        self.current_file = None
        self.tab_editors = {}
        self.setup_ui()
        self.log("Ready.", "INFO")
    
    def setup_ui(self):
        self.setup_menu()
        self.setup_toolbar()
        self.setup_statusbar()
        self.setup_layout()
    
    def setup_menu(self):
        mb = self.menuBar()
        fm = mb.addMenu("&File")
        fm.addAction(QAction("&Open File...", self, shortcut="Ctrl+O", triggered=self.open_file_dialog))
        fm.addAction(QAction("&Save", self, shortcut="Ctrl+S", triggered=self.save_file))
        fm.addAction(QAction("Save &As...", self, triggered=self.save_file_as))
        fm.addSeparator()
        fm.addAction(QAction("New &Editor Tab", self, shortcut="Ctrl+T", triggered=lambda: self.tw.add_new_tab()))
        fm.addAction(QAction("Close Tab", self, shortcut="Ctrl+W", triggered=lambda: self.tw.close_tab(self.tw.currentIndex())))
        fm.addSeparator()
        fm.addAction(QAction("E&xit", self, shortcut="Alt+F4", triggered=self.close))
        
        em = mb.addMenu("&Edit")
        em.addAction(QAction("&Undo", self, shortcut="Ctrl+Z", triggered=lambda: self.edit('undo')))
        em.addAction(QAction("&Redo", self, shortcut="Ctrl+Y", triggered=lambda: self.edit('redo')))
        em.addSeparator()
        em.addAction(QAction("&Search", self, shortcut="Ctrl+F", triggered=self.focus_search))
        
        vm = mb.addMenu("&View")
        vm.addAction(QAction("&Hex View", self, triggered=self.show_hex))
        vm.addAction(QAction("&PE Header", self, triggered=self.show_pe))
        vm.addAction(QAction("E&LF Header", self, triggered=self.show_elf))
        vm.addSeparator()
        vm.addAction(QAction("&Decompile", self, triggered=self.decompile_current))
        
        tm = mb.addMenu("&Tools")
        tm.addAction(QAction("&Batch PYC to PY", self, triggered=self.batch_convert))
    
    def setup_toolbar(self):
        tb = self.addToolBar("Main")
        tb.setMovable(False)
        tb.setIconSize(QSize(16,16))
        bs = """
            QPushButton { 
                background-color: #3A3A3A; 
                color: #CCC; 
                border: 1px solid #555; 
                padding: 4px 10px; 
                border-radius: 3px; 
                font-size: 12px; 
            } 
            QPushButton:hover { 
                background-color: #4A4A4A; 
            }
        """
        for t, f in [("Open", self.open_file_dialog), ("Save", self.save_file)]:
            b = QPushButton(t)
            b.setStyleSheet(bs)
            b.clicked.connect(f)
            tb.addWidget(b)
        tb.addSeparator()
        for t, f in [("Decompile", self.decompile_current), ("Hex", self.show_hex), ("PE", self.show_pe)]:
            b = QPushButton(t)
            b.setStyleSheet(bs)
            b.clicked.connect(f)
            tb.addWidget(b)
        
        sp = QWidget()
        sp.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
        tb.addWidget(sp)
        self.search_bar = SearchBar(self)
        tb.addWidget(self.search_bar)
    
    def setup_statusbar(self):
        self.sb = QStatusBar()
        self.setStatusBar(self.sb)
        self.sl = QLabel("Ready")
        self.sl.setStyleSheet("padding: 0 8px;")
        self.sb.addWidget(self.sl, 1)
        self.fl = QLabel("")
        self.sb.addPermanentWidget(self.fl)
        self.pl = QLabel("")
        self.sb.addPermanentWidget(self.pl)
    
    def setup_layout(self):
        cw = QWidget()
        self.setCentralWidget(cw)
        lo = QHBoxLayout(cw)
        lo.setContentsMargins(2,2,2,2)
        lo.setSpacing(2)
        
        self.ms = QSplitter(Qt.Orientation.Horizontal)
        
        self.ft = QTreeWidget()
        self.ft.setHeaderLabels(["Name","Type","Size"])
        self.ft.setColumnWidth(0,180)
        self.ft.setColumnWidth(1,90)
        self.ft.setColumnWidth(2,60)
        self.ft.itemDoubleClicked.connect(self.on_tree_dblclick)
        self.ft.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.ft.customContextMenuRequested.connect(self.show_context)
        self.ft.keyPressEvent = self.tree_keypress
        self.ms.addWidget(self.ft)
        
        rw = QWidget()
        rl = QVBoxLayout(rw)
        rl.setContentsMargins(0,0,0,0)
        rl.setSpacing(0)
        
        self.editor_splitter = QSplitter(Qt.Orientation.Vertical)
        
        self.tw = EditorTabWidget(self)
        self.tw.currentChanged.connect(self.on_tab_change)
        
        self.tw2 = EditorTabWidget(self)
        self.tw2.currentChanged.connect(lambda idx: self.on_tab_change(idx, bottom=True))
        
        self.editor_splitter.addWidget(self.tw)
        self.editor_splitter.addWidget(self.tw2)
        self.editor_splitter.setSizes([400, 400])
        
        self.con = QTextEdit()
        self.con.setReadOnly(True)
        self.con.setMaximumHeight(100)
        self.con.setStyleSheet("""
            QTextEdit { 
                background-color: #1E1E1E; 
                color: #CCC; 
                border: 1px solid #3F3F3F; 
                font-family: 'Consolas'; 
                font-size: 11px; 
            }
        """)
        
        rl.addWidget(self.editor_splitter, 1)
        rl.addWidget(self.con)
        
        self.ms.addWidget(rw)
        self.ms.setSizes([220,880])
        lo.addWidget(self.ms)
    
    def tree_keypress(self, e):
        if e.key() == Qt.Key.Key_Delete:
            self.delete_item()
        else:
            QTreeWidget.keyPressEvent(self.ft, e)
    
    def delete_item(self):
        sel = self.ft.currentItem()
        if not sel: return
        fp = sel.data(0, Qt.ItemDataRole.UserRole)
        if QMessageBox.question(self, "Delete", f"Remove '{sel.text(0)}'?", 
                               QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
            par = sel.parent() or self.ft
            if isinstance(par, QTreeWidgetItem):
                par.removeChild(sel)
            else:
                self.ft.takeTopLevelItem(self.ft.indexOfTopLevelItem(sel))
            if fp and fp in self.tab_editors:
                del self.tab_editors[fp]
            self.log(f"Removed: {sel.text(0)}")
    
    def log(self, msg, lv="INFO"):
        c = {"INFO":"#CCC","WARN":"#CCA700","ERROR":"#F44747","SUCCESS":"#6A9955"}.get(lv,"#CCC")
        self.con.append(f'<span style="color:#858585">[{datetime.now().strftime("%H:%M:%S")}]</span> <span style="color:{c}">[{lv}]</span> {msg}')
    
    def focus_search(self): 
        self.search_bar.search_input.setFocus()
        self.search_bar.search_input.selectAll()
    
    def get_current_editor(self):
        w = self.tw.currentWidget()
        if isinstance(w, CodeEditor):
            return w
        w = self.tw2.currentWidget()
        return w if isinstance(w, CodeEditor) else None
    
    def edit(self, action):
        ed = self.get_current_editor()
        if isinstance(ed, CodeEditor):
            if action == 'undo': ed.undo()
            elif action == 'redo': ed.redo()
    
    def open_file_dialog(self):
        fp, _ = QFileDialog.getOpenFileName(self, "Open File", "", 
                                           "All Supported (*.exe *.dll *.pyd *.so *.dylib *.pyc *.pyo *.py *.zip);;All Files (*)")
        if fp: self.load_file(fp)
    
    def load_file(self, fp):
        self.current_file = fp
        self.log(f"Loading: {os.path.basename(fp)}")
        self.sl.setText("Analyzing...")
        self.ft.clear()
        self.tw.clear()
        self.tw2.clear()
        self.pd = QProgressDialog("Starting...", "Cancel", 0, 100, self)
        self.pd.setWindowModality(Qt.WindowModality.WindowModal)
        self.pd.setMinimumDuration(0)
        self.pd.show()
        self.ext = ExtractorThread(fp)
        self.ext.progress.connect(lambda v,m: (self.pd.setValue(v), self.pd.setLabelText(m), 
                                               self.sl.setText(m), QApplication.processEvents()))
        self.ext.finished.connect(self.on_done)
        self.ext.error.connect(self.on_err)
        self.ext.start()
    
    def on_done(self, r):
        self.pd.close()
        self.populate_tree(r.get('extract_dir',''))
        self.sl.setText("Loaded")
        self.fl.setText(f"Size: {self._fs(os.path.getsize(self.current_file))} | {r.get('type','?')}")
        self.log(f"Done: {r.get('type','?')}", "SUCCESS")
        
        results = BinaryAnalyzer.detect_and_analyze(self.current_file)
        for title, html in results:
            v = QTextEdit()
            v.setReadOnly(True)
            v.setHtml(html)
            self.tw2.addTab(v, title)
            self.tw2.setCurrentWidget(v)
    
    def on_err(self, e): 
        self.pd.close()
        self.log(f"Error: {e}", "ERROR")
        QMessageBox.critical(self, "Error", str(e))
    
    def populate_tree(self, d, p=None):
        if not d or not os.path.exists(d): return
        if p is None: p = self.ft
        try:
            for i in sorted(os.listdir(d)):
                fp = os.path.join(d,i)
                if os.path.isdir(fp):
                    ti = QTreeWidgetItem(p)
                    ti.setText(0,i)
                    ti.setText(1,"Folder")
                    ti.setExpanded(True)
                    ti.setForeground(0, QColor("#569CD6"))
                    self.populate_tree(fp, ti)
                else:
                    ti = QTreeWidgetItem(p)
                    ti.setText(0,i)
                    ext = os.path.splitext(i)[1].lower()
                    tm = {
                        '.py':'Python','.pyc':'Bytecode','.pyo':'Bytecode',
                        '.pyd':'Extension','.dll':'DLL','.exe':'EXE',
                        '.so':'SO','.dylib':'Dylib','.txt':'Text','.zip':'ZIP'
                    }
                    ft = tm.get(ext,'Binary')
                    ti.setText(1,ft)
                    ti.setText(2,self._fs(os.path.getsize(fp)))
                    ti.setData(0,Qt.ItemDataRole.UserRole,fp)
                    ti.setData(0,Qt.ItemDataRole.UserRole+1,ft)
                    
                    colors = {
                        'Python': '#6A9955',
                        'Bytecode': '#CE9178', 
                        'Extension': '#569CD6',
                        'DLL': '#DCDCAA',
                        'EXE': '#4EC9B0',
                        'Text': '#B5CEA8',
                        'Binary': '#858585'
                    }
                    color = colors.get(ft, '#CCC')
                    ti.setForeground(0, QColor(color))
                    ti.setForeground(1, QColor(color))
        except PermissionError:
            pass
    
    def on_tree_dblclick(self, item, col):
        fp = item.data(0, Qt.ItemDataRole.UserRole)
        if fp and os.path.isfile(fp): 
            self.open_editor(fp, item.data(0, Qt.ItemDataRole.UserRole+1))
    
    def open_editor(self, fp, ft):
        ext = os.path.splitext(fp)[1].lower()
        
        if ft in ('Python','Bytecode','Text') or ext in ('.py','.txt','.json','.xml','.ini','.cfg'):
            tn = os.path.basename(fp)
            ei = self._find_tab(self.tw, tn)
            if ei >= 0: 
                self.tw.setCurrentIndex(ei)
                return
            
            ed = CodeEditor()
            ed.set_search_bar(self.search_bar)
            try:
                with open(fp, 'r', encoding='utf-8', errors='ignore') as f: 
                    ed.setPlainText(f.read())
            except: 
                ed.setPlainText("[Binary]")
            
            ed.setProperty('file_path', fp)
            ed.setProperty('modified', False)
            ed.textChanged.connect(lambda: self.on_mod(ed))
            ed.cursorPositionChanged.connect(lambda: self.on_cursor(ed))
            
            self.tw.addTab(ed, tn)
            self.tw.setCurrentWidget(ed)
            self.tab_editors[fp] = ed
        else:
            tn = f"[HEX] {os.path.basename(fp)}"
            ei = self._find_tab(self.tw2, tn)
            if ei >= 0: 
                self.tw2.setCurrentIndex(ei)
                return
            
            bv = QPlainTextEdit()
            bv.setReadOnly(True)
            bv.setStyleSheet("""
                QPlainTextEdit { 
                    background-color: #1E1E1E; 
                    color: #D4D4D4; 
                    font-family: 'Consolas'; 
                    font-size: 11px; 
                }
            """)
            try:
                with open(fp, 'rb') as f: 
                    data = f.read(1048576)
                res = []
                for i in range(0, len(data), 16):
                    chunk = data[i:i+16]
                    hp = ' '.join(f'{b:02x}' for b in chunk).ljust(48)
                    ap = ''.join(chr(b) if 32<=b<127 else '.' for b in chunk)
                    res.append(f"{i:08x}: {hp} |{ap}|")
                bv.setPlainText('\n'.join(res))
            except: 
                bv.setPlainText("Error")
            
            self.tw2.addTab(bv, tn)
            self.tw2.setCurrentWidget(bv)
    
    def _find_tab(self, tab_widget, tn):
        for i in range(tab_widget.count()):
            if tab_widget.tabText(i) == tn:
                return i
        return -1
    
    def on_mod(self, ed):
        if not ed.property('modified'):
            ed.setProperty('modified', True)
            for tw in [self.tw, self.tw2]:
                if isinstance(tw, QTabWidget):
                    idx = tw.indexOf(ed)
                    if idx >= 0 and not tw.tabText(idx).startswith('*'):
                        tw.setTabText(idx, f"* {tw.tabText(idx)}")
    
    def on_cursor(self, ed):
        c = ed.textCursor()
        self.pl.setText(f"Ln {c.blockNumber()+1}, Col {c.columnNumber()+1}")
    
    def save_file(self):
        ed = self.get_current_editor()
        if isinstance(ed, CodeEditor) and ed.property('file_path'): 
            self._save(ed, ed.property('file_path'))
    
    def save_file_as(self):
        ed = self.get_current_editor()
        if isinstance(ed, CodeEditor):
            op = ed.property('file_path')
            np, _ = QFileDialog.getSaveFileName(self, "Save As", op or "")
            if np: 
                self._save(ed, np)
                ed.setProperty('file_path', np)
    
    def _save(self, ed, fp):
        with open(fp, 'w', encoding='utf-8') as f: 
            f.write(ed.toPlainText())
        ed.setProperty('modified', False)
        for tw in [self.tw, self.tw2]:
            if isinstance(tw, QTabWidget):
                idx = tw.indexOf(ed)
                if idx >= 0:
                    t = tw.tabText(idx)
                    if t.startswith('*'): 
                        tw.setTabText(idx, t[2:])
        self.log(f"Saved: {os.path.basename(fp)}", "SUCCESS")
    
    def show_hex(self):
        if self.current_file:
            tn = f"[HEX] {os.path.basename(self.current_file)}"
            if self._find_tab(self.tw2, tn) >= 0: 
                self.tw2.setCurrentIndex(self._find_tab(self.tw2, tn))
                return
            
            bv = QPlainTextEdit()
            bv.setReadOnly(True)
            bv.setStyleSheet("""
                QPlainTextEdit { 
                    background-color: #1E1E1E; 
                    color: #D4D4D4; 
                    font-family: 'Consolas'; 
                    font-size: 11px; 
                }
            """)
            try:
                with open(self.current_file, 'rb') as f: 
                    data = f.read(2097152)
                res = []
                for i in range(0, len(data), 16):
                    chunk = data[i:i+16]
                    hp = ' '.join(f'{b:02x}' for b in chunk).ljust(48)
                    ap = ''.join(chr(b) if 32<=b<127 else '.' for b in chunk)
                    res.append(f"{i:08x}: {hp} |{ap}|")
                bv.setPlainText('\n'.join(res))
            except: 
                bv.setPlainText("Error")
            
            self.tw2.addTab(bv, tn)
            self.tw2.setCurrentWidget(bv)
    
    def show_pe(self):
        if self.current_file:
            tn = f"PE: {os.path.basename(self.current_file)}"
            if self._find_tab(self.tw2, tn) >= 0: 
                self.tw2.setCurrentIndex(self._find_tab(self.tw2, tn))
                return
            
            v = QTextEdit()
            v.setReadOnly(True)
            v.setHtml(PEAnalyzer.analyze(self.current_file))
            
            self.tw2.addTab(v, tn)
            self.tw2.setCurrentWidget(v)
    
    def show_elf(self):
        if self.current_file:
            tn = f"ELF: {os.path.basename(self.current_file)}"
            if self._find_tab(self.tw2, tn) >= 0: 
                self.tw2.setCurrentIndex(self._find_tab(self.tw2, tn))
                return
            
            v = QTextEdit()
            v.setReadOnly(True)
            v.setHtml(ELFAnalyzer.analyze(self.current_file))
            
            self.tw2.addTab(v, tn)
            self.tw2.setCurrentWidget(v)
    
    def decompile_current(self):
        w = self.tw.currentWidget()
        fp = None
        if w and hasattr(w, 'property'): 
            fp = w.property('file_path')
        if not fp and self.current_file: 
            fp = self.current_file
        if fp and os.path.isfile(fp) and fp.endswith(('.pyc', '.pyo')):
            self.log(f"Decompiling: {os.path.basename(fp)}")
            ok, msg, src, trans = PYCtoPYConverter.convert(fp)
            if ok:
                self.log(msg, "SUCCESS")
                ed_top = CodeEditor()
                ed_top.setReadOnly(True)
                ed_top.setPlainText(src)
                ed_top.set_search_bar(self.search_bar)
                self.tw.addTab(ed_top, f"Bytecode: {os.path.basename(fp)}")
                
                ed_bot = CodeEditor()
                ed_bot.setPlainText(trans)
                ed_bot.set_search_bar(self.search_bar)
                self.tw2.addTab(ed_bot, f"Decompiled: {os.path.basename(fp)}")
                
                self.tw.setCurrentWidget(ed_top)
                self.tw2.setCurrentWidget(ed_bot)
            else: 
                self.log(msg, "ERROR")
    
    def batch_convert(self):
        d = QFileDialog.getExistingDirectory(self, "Select Directory")
        if d:
            files = []
            for r, _, fs in os.walk(d):
                for f in fs:
                    if f.endswith(('.pyc', '.pyo')): 
                        files.append(os.path.join(r, f))
            if not files: 
                QMessageBox.information(self, "None", "No PYC files")
                return
            
            pd = QProgressDialog("Converting...", "Cancel", 0, len(files), self)
            pd.setWindowModality(Qt.WindowModality.WindowModal)
            sc = 0
            for i, f in enumerate(files):
                if pd.wasCanceled(): break
                pd.setValue(i)
                ok, _, _, _ = PYCtoPYConverter.convert(f)
                if ok: sc += 1
            pd.setValue(len(files))
            self.log(f"Batch: {sc} success", "SUCCESS")
            QMessageBox.information(self, "Done", f"Converted {sc}")
    
    def show_context(self, pos):
        item = self.ft.itemAt(pos)
        if not item: return
        fp = item.data(0, Qt.ItemDataRole.UserRole)
        if not fp or not os.path.isfile(fp): return
        
        m = QMenu(self)
        m.setStyleSheet("""
            QMenu { background-color: #2D2D2D; color: #CCC; } 
            QMenu::item:selected { background-color: #094771; }
        """)
        m.addAction("Open in Top", lambda: self.open_editor(fp, item.text(1)))
        m.addAction("Hex View", lambda: self.show_hex_file(fp))
        if fp.endswith(('.pyc', '.pyo')): 
            m.addAction("Decompile", lambda: self.decompile_file(fp))
        m.addSeparator()
        m.addAction("PE Analysis", lambda: self.show_pe_file(fp))
        m.addAction("ELF Analysis", lambda: self.show_elf_file(fp))
        m.addSeparator()
        m.addAction("Remove", lambda: self.delete_item())
        m.addAction("Export...", lambda: self.export_file(fp))
        m.addAction("Copy Path", lambda: QApplication.clipboard().setText(fp))
        m.exec(self.ft.viewport().mapToGlobal(pos))
    
    def show_hex_file(self, fp):
        tn = f"[HEX] {os.path.basename(fp)}"
        if self._find_tab(self.tw2, tn) >= 0: 
            self.tw2.setCurrentIndex(self._find_tab(self.tw2, tn))
            return
        
        bv = QPlainTextEdit()
        bv.setReadOnly(True)
        bv.setStyleSheet("""
            QPlainTextEdit { 
                background-color: #1E1E1E; 
                color: #D4D4D4; 
                font-family: 'Consolas'; 
                font-size: 11px; 
            }
        """)
        try:
            with open(fp, 'rb') as f: 
                data = f.read(1048576)
            res = []
            for i in range(0, len(data), 16):
                chunk = data[i:i+16]
                hp = ' '.join(f'{b:02x}' for b in chunk).ljust(48)
                ap = ''.join(chr(b) if 32<=b<127 else '.' for b in chunk)
                res.append(f"{i:08x}: {hp} |{ap}|")
            bv.setPlainText('\n'.join(res))
        except: 
            bv.setPlainText("Error")
        
        self.tw2.addTab(bv, tn)
        self.tw2.setCurrentWidget(bv)
    
    def show_pe_file(self, fp):
        tn = f"PE: {os.path.basename(fp)}"
        if self._find_tab(self.tw2, tn) >= 0: 
            self.tw2.setCurrentIndex(self._find_tab(self.tw2, tn))
            return
        
        v = QTextEdit()
        v.setReadOnly(True)
        v.setHtml(PEAnalyzer.analyze(fp))
        
        self.tw2.addTab(v, tn)
        self.tw2.setCurrentWidget(v)
    
    def show_elf_file(self, fp):
        tn = f"ELF: {os.path.basename(fp)}"
        if self._find_tab(self.tw2, tn) >= 0: 
            self.tw2.setCurrentIndex(self._find_tab(self.tw2, tn))
            return
        
        v = QTextEdit()
        v.setReadOnly(True)
        v.setHtml(ELFAnalyzer.analyze(fp))
        
        self.tw2.addTab(v, tn)
        self.tw2.setCurrentWidget(v)
    
    def decompile_file(self, fp):
        ok, msg, src, trans = PYCtoPYConverter.convert(fp)
        if ok:
            self.log(msg, "SUCCESS")
            ed_top = CodeEditor()
            ed_top.setReadOnly(True)
            ed_top.setPlainText(src)
            ed_top.set_search_bar(self.search_bar)
            self.tw.addTab(ed_top, f"Bytecode: {os.path.basename(fp)}")
            
            ed_bot = CodeEditor()
            ed_bot.setPlainText(trans)
            ed_bot.set_search_bar(self.search_bar)
            self.tw2.addTab(ed_bot, f"Decompiled: {os.path.basename(fp)}")
            
            self.tw.setCurrentWidget(ed_top)
            self.tw2.setCurrentWidget(ed_bot)
        else: 
            self.log(msg, "ERROR")
    
    def export_file(self, fp):
        dest, _ = QFileDialog.getSaveFileName(self, "Export", os.path.basename(fp))
        if dest: 
            shutil.copy2(fp, dest)
            self.log(f"Exported: {os.path.basename(dest)}", "SUCCESS")
    
    def on_tab_change(self, idx, bottom=False):
        tw = self.tw2 if bottom else self.tw
        if idx >= 0:
            w = tw.widget(idx)
            if isinstance(w, CodeEditor):
                self.on_cursor(w)
    
    def dragEnterEvent(self, e):
        if e.mimeData().hasUrls(): 
            e.acceptProposedAction()
    
    def dropEvent(self, e):
        urls = e.mimeData().urls()
        if urls: 
            self.load_file(urls[0].toLocalFile())
        e.acceptProposedAction()
    
    def _fs(self, s):
        for u in ['B','KB','MB','GB']:
            if s < 1024: return f"{s:.1f} {u}"
            s /= 1024
        return f"{s:.1f} TB"
    
    def closeEvent(self, e):
        unsaved = any(isinstance(ed, CodeEditor) and ed.property('modified') 
                     for ed in self.tab_editors.values())
        if unsaved:
            r = QMessageBox.question(self, "Unsaved", "Save changes?", 
                                   QMessageBox.StandardButton.Save | 
                                   QMessageBox.StandardButton.Discard | 
                                   QMessageBox.StandardButton.Cancel)
            if r == QMessageBox.StandardButton.Save:
                for fp, ed in self.tab_editors.items():
                    if ed.property('modified'): 
                        self._save(ed, fp)
            elif r == QMessageBox.StandardButton.Cancel: 
                e.ignore()
                return
        e.accept()


def main():
    app = QApplication(sys.argv)
    app.setStyle(QStyleFactory.create("Fusion"))
    
    dp = QPalette()
    for role, c in [
        (QPalette.ColorRole.Window,(30,30,30)),
        (QPalette.ColorRole.WindowText,(212,212,212)),
        (QPalette.ColorRole.Base,(30,30,30)),
        (QPalette.ColorRole.Text,(212,212,212)),
        (QPalette.ColorRole.Button,(45,45,45)),
        (QPalette.ColorRole.ButtonText,(212,212,212)),
        (QPalette.ColorRole.Highlight,(9,71,113)),
        (QPalette.ColorRole.HighlightedText,(255,255,255))
    ]:
        dp.setColor(role, QColor(*c))
    app.setPalette(dp)
    
    YhujinPy().show()
    sys.exit(app.exec())

if __name__ == "__main__": 
    main()